from PySide6.QtCore import Slot, Signal, QPoint, QThread
from PySide6.QtGui import QStandardItem, QStandardItemModel, QCloseEvent, QAction
from PySide6.QtWidgets import QWidget, QMessageBox, QMenu
from serial.serialutil import SerialException
from udplistener import UDPListener
from ui_mainform import Ui_Form
import config
import os
import re
import sinodev
import sinodev.tools.generate_mac_address
import sinodev.tools.generate_sn
import sinodev.tools.list_ports
import time
import traceback


SNOW_RES_PATH = os.path.join(os.getcwd(), "result.txt")
SNOW_TEMP_DIR = os.path.join(os.getcwd(), "temp")


class MainForm(QWidget, Ui_Form):
    def __init__(self):
        super().__init__()
        self.setupUi(self)
        self._updateConfigList()
        self.checkBoxConfig.clicked.connect(self.on_checkBoxConfig_clicked)
        self.checkBoxConnect.clicked.connect(self.on_checkBoxConnect_clicked)
        self.tableViewComports.customContextMenuRequested.connect(
            self.on_tableViewComports_customContextMenuRequested
        )

    def _updateConfigList(self):
        for root, _, files in os.walk(os.curdir):
            for file in files:
                if file.lower().endswith((".yaml", ".yml")):
                    self.listWidgetConfig.addItem(os.path.join(root, file))

    def _showError(self, message: str):
        QMessageBox.critical(
            self,
            "Error",
            message,
            QMessageBox.StandardButton.NoButton,
            QMessageBox.StandardButton.NoButton,
        )

    def _showWarning(self, message: str):
        QMessageBox.warning(
            self,
            "Warning",
            message,
            QMessageBox.StandardButton.NoButton,
            QMessageBox.StandardButton.NoButton,
        )

    def _showInformation(self, message: str):
        QMessageBox.information(
            self,
            "Information",
            message,
            QMessageBox.StandardButton.NoButton,
            QMessageBox.StandardButton.NoButton,
        )

    def _isVaildSN(self, sn: str) -> bool:
        return len(sn) == config.g_cfg["sn_length"]

    def _isVaildHWV(self, hwv: str) -> bool:
        return len(hwv) == config.g_cfg["hwv_length"]

    def _isVaildMAC(self, mac: str) -> bool:
        return len(mac) == 17

    def on_checkBoxConfig_clicked(self, checked: bool):
        if checked:
            try:
                config.readcfg(self.listWidgetConfig.currentItem().text())
            except:
                self.checkBoxConfig.setChecked(False)
                self._showError("Failed to load config。")
                return

            self.comboBoxHWV.addItems(config.g_cfg["hwv_list"])
            self.comboBoxHWV0.addItems(config.g_cfg["hwv_list"])
            self.tabOffline.setEnabled(True)
            self.lineEditMAC0.setEnabled(
                config.g_cfg["device_type"] == sinodev.SinoDeviceType.RS
            )
            if config.g_cfg["device_type"] == sinodev.SinoDeviceType.RTK:
                self.tabOnline.setEnabled(True)
                self.tabWidget.setCurrentIndex(1)  # Online
            else:
                self.tabWidget.setCurrentIndex(2)  # Offline

            self.labelConfig.setText("*Save your work before reset.")
        else:
            config.resetcfg()

            self.comboBoxHWV.clear()
            self.comboBoxHWV0.clear()
            self.comboBoxHWV.setCurrentText("")
            self.comboBoxHWV0.setCurrentText("")
            self.lineEditMAC0.setText("")
            self.lineEditSN0.setText("")
            self.plainTextEditLog.clear()
            self.tabOnline.setEnabled(False)
            self.tableViewComports.setModel(None)
            self.tabOffline.setEnabled(False)

            self.labelConfig.clear()

        self.listWidgetConfig.setEnabled(not checked)

    @Slot()
    def on_listWidgetConfig_itemDoubleClicked(self):
        self.checkBoxConfig.click()

    def on_tableViewComports_customContextMenuRequested(self, pos: QPoint):
        menu = QMenu(self)
        deleteRowAction = menu.addAction("Delete")
        deleteRowAction.triggered.connect(self.on_deleteRowAction_triggered)
        menu.exec(self.tableViewComports.mapToGlobal(pos))

    def on_deleteRowAction_triggered(self):
        self.model.removeRow(self.tableViewComports.currentIndex().row())

    @Slot()
    def on_pushButtonRefresh_clicked(self):
        hwv0 = self.comboBoxHWV0.currentText().upper()
        if not self._isVaildHWV(hwv0):
            self._showWarning("Invalid HWV.")
            return

        sn0 = self.lineEditSN0.text().upper()
        if not self._isVaildSN(sn0):
            self._showWarning("Invalid SN.")
            return

        self.model = QStandardItemModel()
        valid_coms = sinodev.tools.list_ports.comports(
            serial_baudrate=config.g_cfg["serial_baudrate"],
            serial_timeout=config.g_cfg["serial_timeout"],
            device_type=config.g_cfg["device_type"],
            device_keywords=config.g_cfg["device_keywords"],
        )
        sn_generator = sinodev.tools.generate_sn.generator(
            len(valid_coms),
            sn0,
            config.g_cfg["sn_tail_step"],
            config.g_cfg["sn_tail_base"],
        )
        if config.g_cfg["device_type"] == sinodev.SinoDeviceType.RS:
            mac0 = self.lineEditMAC0.text().upper()
            if self._isVaildMAC(mac0):
                self._showWarning("Invalid MAC Address.")
                return
            mac_generator = sinodev.tools.generate_mac_address.generator(
                len(valid_coms), mac0
            )
            for com in valid_coms:
                mac = next(mac_generator)
                sn = next(sn_generator)
                self.model.appendRow(
                    [
                        QStandardItem(com),
                        QStandardItem(hwv0),
                        QStandardItem(sn),
                        QStandardItem(mac),
                    ]
                )
        else:
            for com in valid_coms:
                sn = next(sn_generator)
                self.model.appendRow(
                    [QStandardItem(com), QStandardItem(hwv0), QStandardItem(sn)]
                )

        self.tableViewComports.setModel(self.model)

    def _updateColumnSN(self):
        last_sn = self.model.item(self.model.rowCount() - 1, 2).text()
        sn_generator = sinodev.tools.generate_sn.generator(
            self.model.rowCount() + 1,
            last_sn,
            config.g_cfg["sn_tail_step"],
            config.g_cfg["sn_tail_base"],
        )
        next(sn_generator)  # drop last_sn
        for i, sn in enumerate(sn_generator):
            self.model.item(i, 2).setText(sn)
        self.lineEditSN0.setText(self.model.item(0, 2).text())

    def _updateColumnMAC(self):
        last_mac = self.model.item(self.model.rowCount() - 1, 3).text()
        mac_generator = sinodev.tools.generate_mac_address.generator(
            self.model.rowCount(), last_mac
        )
        next(mac_generator)  # drop last_mac
        for i, mac in enumerate(mac_generator):
            self.model.item(i, 3).setText(mac)
        self.lineEditMAC0.setText(self.model.item(0, 3).text())

    @Slot()
    def on_pushButtonWrite_clicked(self):
        try:
            self.model
        except:
            self._showError("Empty table.")
            return

        if (
            QMessageBox.question(
                self,
                "Notice",
                "Always check before write. Are you sure to write?",
                QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
                QMessageBox.StandardButton.Yes,
            )
            == QMessageBox.StandardButton.No
        ):
            return

        try:
            with open(SNOW_RES_PATH, "a", encoding="utf-8") as f:
                for i in range(self.model.rowCount()):
                    com = self.model.item(i, 0).text()
                    hwv, sn = self.model.item(i, 1).text(), self.model.item(i, 2).text()
                    match config.g_cfg["device_type"]:
                        case sinodev.SinoDeviceType.RTK | sinodev.SinoDeviceType.SLAM:
                            with sinodev.SinoRTK(
                                port=com,
                                baudrate=config.g_cfg["serial_baudrate"],
                                timeout=config.g_cfg["serial_timeout"],
                            ) as rtk:
                                rtk.sn_length = config.g_cfg["sn_length"]
                                rtk.hwv_length = config.g_cfg["hwv_length"]
                                if rtk.fill_id() and rtk.set_sn_hwv(
                                    sn,
                                    hwv,
                                    compatibility=config.g_cfg["device_compatibility"],
                                ):
                                    f.write(f"{rtk.hwv}    {rtk.sn}    {rtk.id}")
                        case sinodev.SinoDeviceType.RS:
                            mac = self.model.item(i, 3).text()
                            with sinodev.SinoRS(
                                port=com,
                                baudrate=config.g_cfg["serial_baudrate"],
                                timrout=config.g_cfg["serial_timeout"],
                            ) as rs:
                                rs.sn_length = config.g_cfg["sn_length"]
                                rs.hwv_length = config.g_cfg["hwv_length"]
                                if rs.set_sn_hwv_mac(sn, hwv, mac):
                                    f.write(f"{rs.hwv}    {rs.sn}    {mac}")
                        case sinodev.SinoDeviceType.MS:
                            with sinodev.SinoMS(
                                port=com,
                                baudrate=config.g_cfg["serial_baudrate"],
                                timeout=config.g_cfg["serial_timeout"],
                            ) as ms:
                                ms.sn_length = config.g_cfg["sn_length"]
                                ms.hwv_length = config.g_cfg["hwv_length"]
                                if ms.set_sn_hwv(
                                    sn, hwv, compatibility=config.g_cfg["compatibility"]
                                ):
                                    f.write(f"{ms.hwv}    {ms.sn}")
                    f.write(os.linesep)
            if config.g_cfg["device_type"] == sinodev.SinoDeviceType.RS:
                self._updateColumnMAC()
            self._updateColumnSN()
            self._showInformation("Done. Result updated. Preview updated.")
        except SerialException:
            self._showError(
                "Unable to write. Please check if the device is powered on or if the port is occupied."
            )
        except IndexError:
            self._showError("Unable to write. Invalid return value.")
        except Exception as e:
            traceback.print_exc()
            self._showError(f"Unable to write. Unknown error. {e}")

    def _logAndCatchID(self):
        try:
            os.makedirs(SNOW_TEMP_DIR, exist_ok=True)
            fname = time.strftime("%Y%m%d_%H%M%S")
            log_text = self.plainTextEditLog.toPlainText()
            log_path = os.path.join(SNOW_TEMP_DIR, f"{fname}.log")
            with open(log_path, "w", encoding="utf-8") as logger:
                logger.write(log_text)
            id_path = os.path.join(SNOW_TEMP_DIR, f"{fname}.txt")
            with open(id_path, "w", encoding="utf-8") as catcher:
                all_ids = re.findall(sinodev.SinoCommand.P_I, log_text)
                catcher.write(os.linesep.join(all_ids))
        except Exception as e:
            traceback.print_exc()
            self._showError(str(e))

    def on_signalNotBound_emitted(self):
        self._showWarning("Failed to bind.")
        self.checkBoxConnect.click()

    signalMissionCompleted = Signal()

    def on_checkBoxConnect_clicked(self, checked: bool):
        if checked:
            hwv = self.comboBoxHWV.currentText().upper()
            if not self._isVaildHWV(hwv):
                self.checkBoxConnect.setChecked(False)
                self._showWarning("Invalid HWV.")
                return
            host = self.lineEditHost.text()
            try:
                port = int(self.lineEditPort.text())
            except ValueError:
                self.checkBoxConnect.setChecked(False)
                self._showError("Invalid port.")
                return

            self.udpListener = UDPListener(host, port, hwv)
            self.udpThread = QThread()
            self.udpListener.moveToThread(self.udpThread)
            self.udpListener.signalLogger.connect(self.plainTextEditLog.appendPlainText)
            self.udpListener.signalNotBound.connect(self.on_signalNotBound_emitted)
            self.signalMissionCompleted.connect(
                self.udpListener.on_signalMissionCompleted_emitted
            )
            self.udpThread.started.connect(self.udpListener.on_udpThread_started)
            self.udpThread.finished.connect(self.udpListener.deleteLater)
            self.udpThread.start()

            self.plainTextEditLog.clear()
            self.plainTextEditLog.appendPlainText("I: UDP listener thread is alive.")
        else:
            self.signalMissionCompleted.emit()
            self.udpThread.quit()
            self.udpThread.wait(1000)
            if self.udpThread.isRunning():
                self.plainTextEditLog.appendPlainText(
                    "E: UDP listener thread is still alive!!!"
                )
            else:
                self.plainTextEditLog.appendPlainText("I: UDP listener thread is dead.")

            self._logAndCatchID()

        self.lineEditHost.setEnabled(not checked)
        self.lineEditPort.setEnabled(not checked)
        self.comboBoxHWV.setEnabled(not checked)
        self.tabConfig.setEnabled(not checked)
        self.tabOffline.setEnabled(not checked)

    def closeEvent(self, event: QCloseEvent):
        if self.checkBoxConnect.isChecked():
            event.ignore()
        else:
            event.accept()
